/**************************************************************************
 *
 * Copyright (C) 2007 University of California, Los Angeles.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 *
 *************************************************************************/

package edu.ucla.cs.typecast.app.auction;

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.List;

import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;

import edu.ucla.cs.typecast.net.NetUtil;
import edu.ucla.cs.typecast.util.ConfigurationReader;

/**
 * A dialog that let a buyer place a bid.
 */

class BidDialog extends TextFieldDialog {
	private static final long serialVersionUID = 1L;

	public BidDialog(Frame owner, String title) {
		super(owner, title, new String[] { "Price" });
	}

	public String verifyField(String fieldName, String fieldValue) {
		for (String fn : new String[] { "Price" }) {
			if (fieldName.equals(fn) && !isPositiveInteger(fieldValue)) {
				return fieldName + " must be an integer > 0.";
			}
		}

		return null;
	}
}

/**
 * A dialog shown when a seller wishs to create an auction
 */

class CreateAuctionDialog extends TextFieldDialog {
	private static final long serialVersionUID = 1L;

	public CreateAuctionDialog(JFrame owner, String title) {
		super(owner, title, new String[] { "Item ID", "Description", "Price", "Duration" }, new String[] { "1",
				"IBM Thinkpad X61", "1000", "60" });
	}

	public String verifyField(String fieldName, String fieldValue) {
		for (String fn : new String[] { "Item ID", "Initial Price" }) {
			if (fieldName.equals(fn) && !isPositiveInteger(fieldValue)) {
				return fieldName + " must be an integer > 0.";
			}
		}

		for (String fn : new String[] { "Description", "Duration" }) {
			if (fieldName.equals(fn) && isEmpty(fieldValue)) {
				return fn + " cannot be empty.";
			}
		}

		return null;
	}
}

/**
 * The main GUI application for a buyer/seller.
 */

public class AuctionApplication {
	public static void main(String[] args) {
		SwingUtilities.invokeLater(new AuctionRunner());
	}
}

class AuctionFrame extends JFrame {

	private static final long serialVersionUID = 1L;

	protected static final String[] BUY_TABLE_COLUMNS = new String[] { "ID", "Description", "Seller", "Highest Bidder",
			"Price", "Time Left" };

	protected static final String[] SELL_TABLE_COLUMNS = new String[] { "ID", "Description", "Bidder", "Price",
			"Time Left" };

	protected PlayerImpl player;

	protected JButton openAuctionButton, closeAuctionButton, bidButton;

	protected DefaultTableModel buyDM, sellDM;

	protected JTable buyTable, sellTable;

	/**
	 * The last hash value of the auction list of the participant.
	 */

	protected int tableHash;

	public AuctionFrame(PlayerImpl player) {
		super();
		this.player = player;

		//

		sellDM = new AuctionTableModel();
		sellDM.setColumnIdentifiers(SELL_TABLE_COLUMNS);

		sellTable = new AuctionTable(sellDM);
		sellTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
			public void valueChanged(ListSelectionEvent arg0) {
				buyTable.getSelectionModel().clearSelection();
				updateButtons();
			}
		});

		//

		buyDM = new AuctionTableModel();
		buyDM.setColumnIdentifiers(BUY_TABLE_COLUMNS);

		buyTable = new AuctionTable(buyDM);
		buyTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
			public void valueChanged(ListSelectionEvent arg0) {
				sellTable.getSelectionModel().clearSelection();
				updateButtons();
			}
		});

		//

		openAuctionButton = new JButton("Open");
		openAuctionButton.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent event) {
				onOpenAuctionButtonClick();
			}
		});

		//

		closeAuctionButton = new JButton("Close");
		closeAuctionButton.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent event) {
				onCloseAuctionButtonClick();
			}
		});

		//

		bidButton = new JButton("Bid");
		bidButton.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent event) {
				onBidButtonClick();
			}
		});

		//

		JPanel buttonPanel = new JPanel(new FlowLayout());
		buttonPanel.add(openAuctionButton);
		buttonPanel.add(closeAuctionButton);
		buttonPanel.add(bidButton);

		JPanel tablePanel = new JPanel(new GridLayout(0, 1));
		tablePanel.add(new JScrollPane(sellTable));
		tablePanel.add(new JScrollPane(buyTable));

		getContentPane().setLayout(new BorderLayout());
		getContentPane().add(tablePanel, BorderLayout.CENTER);
		getContentPane().add(buttonPanel, BorderLayout.SOUTH);

		setTitle("Ad-hoc Marketplace");
		setSize(600, 400);

	}

	/**
	 * Calculate a hash value for a auctioin info list.
	 * 
	 * @param listings
	 * @return
	 */

	protected int calcHash(Collection<ListingInfo> listings) {
		int result = 0;
		for (ListingInfo listing : listings) {
			result ^= listing.getItemInfo().getId();
			result ^= listing.getPrice();
			result ^= listing.getEndDate().getTime();
		}
		return result;
	}

	protected String[] createBuyTableRow(ListingInfo listing) {
		String[] result = new String[6];

		result[0] = "" + listing.getItemInfo().getId();
		result[1] = listing.getItemInfo().getDescription();

		ParticipantInfo seller = listing.getSellerInfo();
		result[2] = seller != null ? seller.getName() : "(none)";

		ParticipantInfo highestBidder = listing.getHighestBidderInfo();
		result[3] = highestBidder != null ? highestBidder.getName() : "(none)";

		result[4] = "" + listing.getPrice();

		result[5] = (getTimeLeft(listing) > 0 ? Long.toString(getTimeLeft(listing)) : "Ended");

		return result;
	}

	protected String[] createSellTableRow(ListingInfo listing) {
		String[] result = new String[5];

		result[0] = "" + listing.getItemInfo().getId();
		result[1] = listing.getItemInfo().getDescription();

		ParticipantInfo hBidder = listing.getHighestBidderInfo();
		result[2] = hBidder != null ? hBidder.getName() + "@" + hBidder.getHost().getHostAddress() + ":"
				+ hBidder.getPort() : "(none)";

		result[3] = "" + listing.getPrice();

		result[4] = (getTimeLeft(listing) > 0 ? Long.toString(getTimeLeft(listing)) : "Ended");

		return result;
	}

	public ListingInfo findInBuyListings(int selectedIndex) {
		int idx = -1;
		for (ListingInfo listing : player.getAuctionInfoList()) {
			if (listing.getSellerInfo().getId() != player.getInfo().getId()) {
				idx++;
				if (idx == selectedIndex) {
					return listing;
				}
			}
		}
		return null;
	}

	public ListingInfo findInSellListings(int selectedIndex) {
		int idx = -1;
		for (ListingInfo listing : player.getAuctionInfoList()) {
			if (listing.getSellerInfo().getId() == player.getInfo().getId()) {
				idx++;
				if (idx == selectedIndex) {
					return listing;
				}
			}
		}
		return null;
	}

	public PlayerImpl getPlayer() {
		return player;
	}

	protected long getTimeLeft(ListingInfo listing) {
		return (listing.getEndDate().getTime() - new Date().getTime()) / 1000;
	}

	public void onBidButtonClick() {
		// Do nothing if no item selected to bid on

		if (buyTable.getSelectedRow() == -1)
			return;

		ListingInfo listing = findInBuyListings(buyTable.getSelectedRow());
		if (listing == null)
			return;

		//

		BidDialog dialog = new BidDialog(this, "Bid");
		dialog.setVisible(true);

		if (!dialog.getResult().equals(BidDialog.Result.OK))
			return;

		//

		int price = Integer.parseInt(dialog.getFieldValue("Price"));

		try {

			player.bid(listing.getSellerInfo(), listing.getItemInfo(), price);
		} catch (IOException e) {
			JOptionPane.showMessageDialog(dialog, e.getMessage());
		}

		//

		updateTable();
	}

	public void onCloseAuctionButtonClick() {
		// Do nothing if no item selected to close

		if (sellTable.getSelectedRow() == -1)
			return;

		ListingInfo listing = findInSellListings(sellTable.getSelectedRow());
		if (listing == null)
			return;

		//

		player.closeAuction(listing.getItemInfo());

		//

		updateTable();
	}

	public void onOpenAuctionButtonClick() {
		//

		CreateAuctionDialog dialog = new CreateAuctionDialog(this, "Create Auction");
		dialog.setVisible(true);

		if (!dialog.getResult().equals(CreateAuctionDialog.Result.OK))
			return;

		//

		int itemId = Integer.parseInt(dialog.getFieldValue("Item ID"));
		for (ListingInfo listing : player.getAuctionInfoList()) {
			if (listing.getItemInfo().getId() == itemId) {
				JOptionPane.showMessageDialog(this, "Item #" + itemId + " is in use.");
				return;
			}
		}

		//

		ItemInfo itemInfo = new ItemInfo(Integer.parseInt(dialog.getFieldValue("Item ID")), dialog
				.getFieldValue("Description"));
		int price = Integer.parseInt(dialog.getFieldValue("Price"));
		Date endDate = new Date(new Date().getTime() + Integer.parseInt(dialog.getFieldValue("Duration")) * 1000);
		player.openAuction(itemInfo, price, endDate);

		//

		updateTable();
	}

	protected void updateButtons() {
		closeAuctionButton.setEnabled(sellTable.getSelectedRow() != -1);
		bidButton.setEnabled(buyTable.getSelectedRow() != -1);
	}

	/**
	 * Update the sell table fully (redraw everything).
	 */

	protected void updateBuyTableFull() {
		if (player != null && player.getAuctionInfoList() != null) {
			List<Object[]> cells = new ArrayList<Object[]>();

			for (ListingInfo listing : player.getAuctionInfoList()) {
				if (!listing.getSellerInfo().equals(player.getInfo()))
					cells.add(createBuyTableRow(listing));
			}

			buyDM.setDataVector(cells.toArray(new Object[][] {}), BUY_TABLE_COLUMNS);
		}
	}

	/**
	 * Only update the time left in the sell table.
	 */

	protected void updateBuyTableMinimal() {
		int idx = -1;
		for (ListingInfo listing : player.getAuctionInfoList()) {
			if (!listing.getSellerInfo().equals(player.getInfo())) {
				idx++;
				buyDM.setValueAt(getTimeLeft(listing) > 0 ? Long.toString(getTimeLeft(listing)) : "Ended", idx, 5);
			}
		}
	}

	/**
	 * Update the sell table fully (redraw everything).
	 */

	protected void updateSellTableFull() {
		if (player != null && player.getAuctionInfoList() != null) {
			List<Object[]> cells = new ArrayList<Object[]>();

			for (ListingInfo listing : player.getAuctionInfoList()) {
				if (listing.getSellerInfo().equals(player.getInfo())) {
					cells.add(createSellTableRow(listing));
				}
			}

			sellDM.setDataVector(cells.toArray(new Object[][] {}), SELL_TABLE_COLUMNS);
		}
	}

	/**
	 * Only update the time left in the sell table.
	 */

	protected void updateSellTableMinimal() {
		int idx = -1;
		for (ListingInfo listing : player.getAuctionInfoList()) {
			if (listing.getSellerInfo().equals(player.getInfo())) {
				idx++;
				sellDM.setValueAt(getTimeLeft(listing) > 0 ? Long.toString(getTimeLeft(listing)) : "Ended", idx, 4);
			}
		}
	}

	/**
	 * Update the auction table in the GUI.
	 */

	public void updateTable() {
		if (player != null && player.getAuctionInfoList() != null && tableHash == calcHash(player.getAuctionInfoList())) {
			updateBuyTableMinimal();
			updateSellTableMinimal();
		} else {
			updateBuyTableFull();
			updateSellTableFull();
		}

		if (player != null && player.getAuctionInfoList() != null) {
			tableHash = calcHash(player.getAuctionInfoList());
		}

		updateButtons();
	}
}

class AuctionRunner implements Runnable {
	protected PlayerImpl player;

	public void loadConfig(Reader rd) throws IOException {
		ConfigurationReader cfr = new ConfigurationReader(rd);
		cfr.setDefaultValue("");

		// Player ID

		int id;
		try {
			id = Integer.parseInt(cfr.get("player.id"));
			log("info", "player ID is " + id);
		} catch (NumberFormatException e) {
			id = 1000000 + (int) (Math.random() * 8999999);
			log("info", "player ID not given, set to " + id);
		}

		// Player name

		String name = cfr.get("player.name");
		if (!name.equals("")) {
			log("info", "player name is " + name);
		} else {
			name = "Player" + id;
			log("info", "player name not given, set to " + name);
		}

		// Private hostname

		InetAddress privateHost;
		try {
			if (cfr.get("player.private_host").equals(""))
				throw new UnknownHostException();
			privateHost = InetAddress.getByName(cfr.get("player.private_host"));
			log("info", "private host is " + privateHost.getHostName());
		} catch (UnknownHostException e1) {
			privateHost = NetUtil.getLocalHost();
			log("info", "private host not given, set to " + privateHost.getHostName() + " ("
					+ privateHost.getHostAddress() + ")");
		}

		// Private port

		int privatePort;
		try {
			privatePort = Integer.parseInt(cfr.get("player.private_port"));
			log("info", "private port is " + privatePort);
		} catch (NumberFormatException e) {
			privatePort = 10000 + (int) (Math.random() * 49999);
			log("info", "private port not given, set to " + privatePort);
		}

		// Public hostname

		InetAddress publicHost;
		try {
			if (cfr.get("player.public_host").equals(""))
				throw new UnknownHostException();
			publicHost = InetAddress.getByName(cfr.get("player.public_host"));
			log("info", "public host is " + publicHost);
		} catch (UnknownHostException e1) {
			publicHost = InetAddress.getByName("239.255.10.10");
			log("info", "public host not given, set to 239.255.10.10");
		}

		// Public port

		int publicPort;
		try {
			publicPort = Integer.parseInt(cfr.get("player.public_port"));
			log("info", "public port is " + publicPort);
		} catch (NumberFormatException e) {
			publicPort = 5265;
			log("info", "public port not given, set to 5265");
		}

		// Create a new player

		player = new PlayerImpl(new ParticipantInfo(id, name, privateHost, privatePort), publicHost, publicPort);
	}

	protected void log(String type, String msg) {
		System.out.println("[" + type + "] " + msg);
	}

	public void run() {
		// Load the config file if found

		try {
			if (new File("auction.conf").exists()) {
				loadConfig(new FileReader("auction.conf"));
			} else {
				loadConfig(new StringReader(""));
			}
		} catch (IOException e) {
			e.printStackTrace();
			return;
		}

		// Show the auction window

		final AuctionFrame frame = new AuctionFrame(player);
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		frame.setVisible(true);

		// Create a timer thread

		Thread timerThread = new Thread() {
			public void run() {
				int count = 0;
				while (true) {
					// Send "auction update" every 30 seconds

					count++;
					if (count % 30 == 0) {
						frame.getPlayer().update();
					}

					// Update the displayed tables

					frame.updateTable();

					// See you next second

					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
					}
				}
			}
		};
		timerThread.setPriority(Thread.MIN_PRIORITY);
		timerThread.setDaemon(true);
		timerThread.start();
	}
}

class AuctionTable extends JTable {
	private static final long serialVersionUID = 1L;

	public AuctionTable(DefaultTableModel dm) {
		super(dm);
		setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
		setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
		setRowSelectionAllowed(true);
		setColumnSelectionAllowed(false);
	}
}

class AuctionTableModel extends DefaultTableModel {
	private static final long serialVersionUID = 1L;

	public boolean isCellEditable(int rowIndex, int colIndex) {
		return false;
	}
}